第16章 深度学习初窥之神经网络模型¶

深度学习(Deep Learning)是机器学习(Machine Learning)的一个分支, 和机器学习一样, 深度学习也是为人工智能服务的, 击败众多世界顶级围棋高手的AlphaGo背后的原理就是深度学习

在数据量较小时, 深度学习算法的效果和传统人工智能算法的效果相差不大, 但在面对庞大的数据时, 深度学习算法的效果会优于传统人工智能算法的效果。因此, 当数据量非常庞大时, 深度学习能满足的场景更多, 应用的范围更广

本章不会过多地介绍目前常用的TensorFlow、Keras、PyTorch等深度学习框架, 而是将重点放在深度学习的基础——神经网络模型上, 通过学习简单的神经网络模型, 初步了解深度学习

16.1 深度学习基础: 神经网络模型¶

深度学习的框架基础是神经网络模型, 但它研究的不是简单的神经网络模型, 而是多层隐藏层的深度神经网络

16.1.1 神经网络模型的基本原理¶

神经网络模型其实是在模仿人类大脑思考的方式: 神经元是神经系统最基本的结构和功能单位, 分为突起和细胞体两部分, 突起的作用是接收冲动并传递给细胞体, 细胞体整合输入的信息并传出, 人类的大脑在思考时, 神经元会接收外部的刺激, 当传入的冲动使神经元的电位超过阈值时, 神经元就会从抑制转向兴奋, 并将信号向下一个神经元传导, 神经网络模型的思想是通过构造人造神经元的方式模拟这一过程

1. 单层神经网络模型¶

如图所示, 在一个简单的神经网络模型中有两组神经元, 一组接收信号, 一组输出信号, 接收信号的一组神经元通过线性变换和非线性的激活函数转换来修改信号, 并传递给下一组神经元

image.png

这种由两组神经元构成的简单神经网络模型也称为单层神经网络模型, 其输出层信号的计算分为两步:

  1. 对输入的信号进行加权平均, 公式为:

    $$ y' = W_1X_1 + W_2X_2 + \cdots + W_nX_n $$

    容易发现, 此时如果不进行后续操作, 那么它就和第3章的线性回归模型是一样的, 其实神经网络模型的确和之前学到的一些基础模型有着紧密的联系

  2. 对加权平均的结果使用激活函数(Activation Function) $\Phi(x)$ 进行非线性转换, 计算输出值, 公式为:

    $$ y = \Phi(y') $$

    因为如果只是单纯的线性转换, 就和之前讲过的线性回归模型差别不大了, 所以这里要通过激活函数进行非线性转换, 所谓非线性转换其实就是区别于 $ax+b$ 这种传统的线性组合的转换, 例如, 通过 $x^2$ 的转换就是一个典型的非线性转换, 而所谓激活函数就是做非线性转换时用到的函数

    在神经网络模型中, 常用来做非线性转换的激活函数有Sigmoid函数、Tanh函数、Relu函数:

    • Sigmoid函数: 该函数是将取值为 $(-\infty, \infty)$ 的数转换到 $(0,1)$ 之间, 可以用来做二分类, Sigmoid函数的导数 $f'(x)$ 从0开始, 很快又趋近于0, 所以在梯度下降时会出现梯度消失, 而且Sigmoid函数的均值是0.5而非0, 不利于下一层的输出

      image-2.png

      对于单层神经网络模型来说, 如果采用Sigmoid函数作为激活函数进行非线性转换, 此时单层神经网络模型的数学原理和第4章所讲的逻辑回归模型的数学原理一样, 也就是说, 此时的单层神经网络模型就是逻辑回归模型

    • Tanh函数: Tanh函数将取值为 $(-\infty, \infty)$ 的数转换到 $(-1,1)$ 之间, 当 $x$ 很大或很小时, 导数 $f'(x)$ 也会很接近0, 同样有梯度消失的问题, 但是Tanh函数的均值为0, 这弥补了Sigmoid函数均值为0.5的缺点

      image-3.png

    • Relu函数: Relu函数是一种分段线性函数, 当输入为正数时, 它可以弥补Sigmoid函数和Tanh函数的梯度消失问题, 但是当输入为负数时, 它仍然有梯度消失的问题, 此外, Relu函数的计算速度比Sigmoid函数和Tanh函数要快一些, 在实际应用中, Relu函数在神经网络模型中的应用范围相对较广

      image-4.png

2. 多层神经网络模型¶

在实际应用中, 神经网络模型往往都不是单层的, 而是使用如下图示的多层神经网络模型, 在多层神经网络模型中, 输入层和输出层之间可以有多个隐藏曾, 层与层之间相互连接, 信号不断地从上一层传递到下一层, 每层的结果都通过线性变换和激活函数的非线性变换得到, 最后由输出层输出, 这些数量众多的隐藏层也是深度学习中 "深度" 两个字的由来

image.png

16.1.2 神经网络模型的简单代码实现¶

下面使用Scikit-Learn库中的MLP多层神经网络模型解决一个简单的二分类问题, 演示数据见下, 其中二维向量 $X$ 是自变量, $Y$ 是因变量, 值为0或1, 代表2个不同的分类

image.png

In [1]:
X = [[1, 0], [5, 1], [6, 4], [4, 2], [3, 2]]
y = [0, 1, 1, 0, 0]
In [2]:
import warnings
warnings.filterwarnings('ignore')
In [3]:
from sklearn.neural_network import MLPClassifier
mlp =MLPClassifier()
mlp.fit(X, y)
Out[3]:
MLPClassifier()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
MLPClassifier()
In [4]:
y_pred = mlp.predict(X)
In [5]:
import pandas as pd
a = pd.DataFrame()  # 创建一个空DataFrame 
a['预测值'] = list(y_pred)
a['实际值'] = list(y)
In [6]:
a
Out[6]:
预测值 实际值
0 0 0
1 1 1
2 1 1
3 0 0
4 0 0

可以看出, 对该简单二分类问题的预测准确度达到了100%

因为这里只是简单演示神经网络模型的代码实现, 前面没有进行训练集和测试集的划分, 而机器学习模型在训练集上的预测准确度一般都非常高, 所以这个100% 的预测准确度并没有实际价值

补充知识点: 神经网络回归模型¶

神经网络模型除了可以用于分类分析, 还可以用于回归分析

In [7]:
from sklearn.neural_network import MLPRegressor
X = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
y = [1, 2, 3, 4, 5]

model = MLPRegressor(random_state=123)  # 设置random_state随机状态参数, 使得每次训练的模型都是一样的
model.fit(X, y)

print(model.predict([[5, 5]]))
[2.85598566]

16.2 案例实战: 用户评论情感分析模型¶

这节通过搭建一个电商平台的用户评论情感分析模型来讲解如何在实战中应用简单的神经网络模型

16.2.1 案例背景¶

用户在电商平台发布的产品评价和评分中包含着用户的偏好信息, 利用情感分析模型可以从产品评价和评分中获取用户的情感及对产品属性的偏好, 在此基础上, 就可以进一步利用智能推荐系统向用户推荐更多他们喜欢的产品, 以增加用户的黏性, 挖掘潜在利润

16.2.2 数据读取、中文分词、文本向量化¶

先读取某电商平台上销售的iPhone XR的1080条评价, 其中有600条好评, 480条差评, "评价" 列中1代表好评, 0代表差评

In [8]:
import pandas as pd
df = pd.read_excel('产品评价.xlsx')
df.head()
Out[8]:
客户编号 评论 评价
0 1 是iPhone8 XR正品,按键屏幕反应蛮快的很灵活,屏幕6.0的不算很大,刚刚好,这款面容... 1
1 2 外形外观:外光非常漂亮,黑色的非常大气。适合男士拥有。屏幕音效:刚开机就下载了一个QQ音乐试... 1
2 3 从苹果4s,到6s,再到xr,就是喜欢苹果的手感和风格,视频流畅,图片清晰,纠结了好久买哪个... 1
3 4 主要是手感,太沉了,比苹果6,沉一倍,厚太多了,看中双卡双待机,刚买回来用,待机时间还不错,... 1
4 5 外形外观:红色超级好看,送妈妈的。屏幕音效:音效还可以,也什么特别的,屏幕看着也挺舒服。拍照... 1
In [9]:
import warnings 
warnings.filterwarnings('ignore')

和第13章新闻聚类分群模型案例一样, 文本数据不能直接拿来训练, 需要将文本分词, 构建词频矩阵, 再用来拟合模型, 利用第13章的jieba库可以进行中文分词

In [10]:
# 通过第2章讲的iloc获取数据表DataFrame第一行信息,0表示第一行
df.iloc[0]
Out[10]:
客户编号                                                    1
评论      是iPhone8 XR正品,按键屏幕反应蛮快的很灵活,屏幕6.0的不算很大,刚刚好,这款面容...
评价                                                      1
Name: 0, dtype: object
In [11]:
# jieba库分词示例
import jieba
# word = list(jieba.cut(df.iloc[0]['评论']))

word = ' '.join(jieba.cut(df.iloc[0]['评论']))

word
Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\19559\AppData\Local\Temp\jieba.cache
Loading model cost 0.518 seconds.
Prefix dict has been built successfully.
Out[11]:
'是 iPhone8   XR 正品 , 按键 屏幕 反应 蛮快 的 很 灵活 , 屏幕 6.0 的 不算 很大 , 刚刚 好 , 这 款 面容 识别 开锁 比 指纹 方便 多 了 , 内外 的 整体 看起来 很 美观 , 整机 子 不算 是 很厚感 , 像素 高 比较 清晰 , 双卡 双待 , 续航 强 , 跟 8plus 差价 300 元 , 还是 选 XR 款好 , 性能 不错 , 处理器 、 芯片 也 是 最新 一代'

可以看到, 此时已经将第1条评论分词完毕, 下面对所有评论进行分词

In [12]:
# 遍历整张表格,对所有评论进行分词
words = []
for i, row in df.iterrows():
    word = jieba.cut(row['评论'])
    result = ' '.join(word) 
    words.append(result)
In [13]:
words[0:2]
Out[13]:
['是 iPhone8   XR 正品 , 按键 屏幕 反应 蛮快 的 很 灵活 , 屏幕 6.0 的 不算 很大 , 刚刚 好 , 这 款 面容 识别 开锁 比 指纹 方便 多 了 , 内外 的 整体 看起来 很 美观 , 整机 子 不算 是 很厚感 , 像素 高 比较 清晰 , 双卡 双待 , 续航 强 , 跟 8plus 差价 300 元 , 还是 选 XR 款好 , 性能 不错 , 处理器 、 芯片 也 是 最新 一代',
 '外形 外观 : 外光 非常 漂亮 , 黑色 的 非常 大气 。 适合 男士 拥有 。 屏幕 音效 : 刚 开机 就 下载 了 一个 QQ 音乐 试 了 一下 。   音效 还是 非常 不错 的 。 拍照 效果 : 拍照 很 清晰 , 照亮 你 脸上 的 痘痘 。 运行 速度 : 运行 速度 就 不用说 了 。   一个 字快 。 待机时间 : 待机 很 不错 。 用 一段时间 再 来 评价 。 其他 特色 : 个人感觉 比 X 好 。   可能 是因为 上手 的 手感 比较 好 吧 , 总之 还是 值得 入手 的']
In [14]:
# 如果对上面过程如果熟悉后,也可以直接写成如下的合并代码形式
words = []
for i, row in df.iterrows():
    words.append(' '.join(jieba.cut(row['评论'])))

words[0:2]
Out[14]:
['是 iPhone8   XR 正品 , 按键 屏幕 反应 蛮快 的 很 灵活 , 屏幕 6.0 的 不算 很大 , 刚刚 好 , 这 款 面容 识别 开锁 比 指纹 方便 多 了 , 内外 的 整体 看起来 很 美观 , 整机 子 不算 是 很厚感 , 像素 高 比较 清晰 , 双卡 双待 , 续航 强 , 跟 8plus 差价 300 元 , 还是 选 XR 款好 , 性能 不错 , 处理器 、 芯片 也 是 最新 一代',
 '外形 外观 : 外光 非常 漂亮 , 黑色 的 非常 大气 。 适合 男士 拥有 。 屏幕 音效 : 刚 开机 就 下载 了 一个 QQ 音乐 试 了 一下 。   音效 还是 非常 不错 的 。 拍照 效果 : 拍照 很 清晰 , 照亮 你 脸上 的 痘痘 。 运行 速度 : 运行 速度 就 不用说 了 。   一个 字快 。 待机时间 : 待机 很 不错 。 用 一段时间 再 来 评价 。 其他 特色 : 个人感觉 比 X 好 。   可能 是因为 上手 的 手感 比较 好 吧 , 总之 还是 值得 入手 的']
In [15]:
# # iterrows()函数相关知识点,不熟悉DataFrame数据表遍历的话,可以把下面的注释取消了,看看效果
# for i, row in df.iterrows():
#     print(i)
#     print(row)

下面用13.3.2中的CountVectorizer()函数对words列表中的分词结果进行文本向量化

In [16]:
# 文本向量化CountVectorizer()函数的使用技巧:使用示例
from sklearn.feature_extraction.text import CountVectorizer
test = ['手机 外观 漂亮 非常', '手机 图片 清晰']
vect = CountVectorizer()
X = vect.fit_transform(test)

# for i in X:
#     print(i)
# X = X.toarray()
In [17]:
print(X)

print(type(X))
  (0, 2)	1
  (0, 1)	1
  (0, 4)	1
  (0, 5)	1
  (1, 2)	1
  (1, 0)	1
  (1, 3)	1
<class 'scipy.sparse._csr.csr_matrix'>
In [18]:
print(X.toarray())

X = X.toarray()
[[0 1 1 0 1 1]
 [1 0 1 1 0 0]]
In [19]:
words_bag = vect.vocabulary_
# print(words_bag)

# print(words_bag)

# print(list(words_bag.items()).sort())

# 按值排序
words_bag = dict(sorted(words_bag.items(), key=lambda item: item[1]))
print(words_bag)

# print(words_bag.keys())
{'图片': 0, '外观': 1, '手机': 2, '清晰': 3, '漂亮': 4, '非常': 5}
In [20]:
# 忽略这部分

a = [i[1] for i in words_bag.items()]

print(a)

a = [i for i in words_bag.items()]

print(a)

a = sorted([1,2,3,-2.5], key=lambda x: x**2)

print(a)
[0, 1, 2, 3, 4, 5]
[('图片', 0), ('外观', 1), ('手机', 2), ('清晰', 3), ('漂亮', 4), ('非常', 5)]
[1, 2, -2.5, 3]
In [21]:
X = pd.DataFrame(columns=words_bag.keys(), data = X)

X
Out[21]:
图片 外观 手机 清晰 漂亮 非常
0 0 1 1 0 1 1
1 1 0 1 1 0 0
In [22]:
# 也忽略这部分

import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer

# 示例训练文本
test = ['手机 外观 漂亮 非常', '手机 图片 清晰']

# 自定义函数,生成按词首次出现顺序排列的词汇表
def get_ordered_vocabulary(texts):
    ordered_words = []
    for text in texts:
        words = text.split()
        for word in words:
            if word not in ordered_words:
                ordered_words.append(word)
    return {word: index for index, word in enumerate(ordered_words)}

# 生成有序的词汇表
ordered_words_bag = get_ordered_vocabulary(test)

# 使用 CountVectorizer 时,传入自定义的词汇表
vect = CountVectorizer(vocabulary=ordered_words_bag.keys())  # 关键修改
X = vect.fit_transform(test)

# 按照有序词汇表的顺序排列 DataFrame 的列
X = pd.DataFrame(columns=list(ordered_words_bag.keys()), data=X.toarray())

print("有序的词汇表:", ordered_words_bag)
print("按顺序排列的 DataFrame:")
print(X)
有序的词汇表: {'手机': 0, '外观': 1, '漂亮': 2, '非常': 3, '图片': 4, '清晰': 5}
按顺序排列的 DataFrame:
   手机  外观  漂亮  非常  图片  清晰
0   1   1   1   1   0   0
1   1   0   0   0   1   1
In [23]:
# 实际应用
from sklearn.feature_extraction.text import CountVectorizer
vect = CountVectorizer()
X = vect.fit_transform(words)
X = X.toarray()
print(X)
[[0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 ...
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]]

因为评论文本较多, 分此后得到的不同词语很多, 所以X中很多地方都是0

In [24]:
words_bag = vect.vocabulary_

print(list(words_bag.items())[:20])
[('iphone8', 194), ('xr', 264), ('正品', 2660), ('按键', 2221), ('屏幕', 1798), ('反应', 1210), ('蛮快', 3492), ('灵活', 2843), ('不算', 517), ('很大', 1967), ('刚刚', 1031), ('面容', 3979), ('识别', 3570), ('开锁', 1915), ('指纹', 2218), ('方便', 2362), ('内外', 941), ('整体', 2341), ('看起来', 3101), ('美观', 3345)]

所得的词袋就是一个字典, 其内容是对评论的分词结果进行去重, 再对不同的词进行编号

In [25]:
words_bag = dict(sorted(words_bag.items(), key = lambda item: item[1],reverse=False))

print(list(words_bag.items())[1000:1020])

# 似乎对汉字排序了
[('出事', 1000), ('出仓', 1001), ('出厂', 1002), ('出名', 1003), ('出品', 1004), ('出售', 1005), ('出奇', 1006), ('出小', 1007), ('出手', 1008), ('出来', 1009), ('出汗', 1010), ('出现', 1011), ('出色', 1012), ('出超', 1013), ('出错', 1014), ('出门', 1015), ('出门在外', 1016), ('分别', 1017), ('分期', 1018), ('分辨', 1019)]
In [26]:
len(words_bag)
Out[26]:
4075
In [27]:
import pandas as pd
# pd.set_option('display.max_columns', None)  # 添加这行代码可以显示所有列,如果讲None改成500,则表示可最多显示500列
# pd.set_option('display.max_rows', None)  # 添加这行代码可以显示所有行,如果讲None改成500,则表示可最多显示500行
X = pd.DataFrame(X, columns=words_bag.keys())

X.head()
Out[27]:
00 0022 0k 10 100 1000 10050 1080 1080p 10w ... 黑点 黑白 黑粉 黑色 黑边 默认 鼓捣 齐全 齐备 龟速
0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 1 0 0 0 0 0 0
2 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

5 rows × 4075 columns

In [28]:
# 忽略这个

non_zero_column_names = [col for col, value in X.iloc[0].items() if value != 0]

print(non_zero_column_names)
['300', '8plus', 'iphone8', 'xr', '一代', '不算', '不错', '像素', '内外', '刚刚', '双卡', '双待', '反应', '处理器', '屏幕', '差价', '开锁', '很厚感', '很大', '性能', '指纹', '按键', '整体', '整机', '方便', '最新', '款好', '正品', '比较', '清晰', '灵活', '看起来', '续航', '美观', '芯片', '蛮快', '识别', '还是', '面容']
In [29]:
y = df['评价']
y.head()
Out[29]:
0    1
1    1
2    1
3    1
4    1
Name: 评价, dtype: int64

16.2.3 神经网络模型的搭建与使用¶

划分训练集与测试集

In [30]:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=1)
In [31]:
from sklearn.neural_network import MLPClassifier
mlp =MLPClassifier(random_state=123)  # 因为模型运行具有随机性,如果想让每次运行结果一致,可以设置random_state随机参数为任一数字,如MLPClassifier(random_state=123)
mlp.fit(X_train, y_train)
Out[31]:
MLPClassifier(random_state=123)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
MLPClassifier(random_state=123)
In [32]:
y_pred = mlp.predict(X_test)
print(y_pred)  # 因为模型运行具有随机性,所以这里得到的结果可能和书上的略有不同,如果想让每次运行结果一致,可以设置random_state随机参数为任一数字,如MLPClassifier(random_state=123)
[1 0 0 1 0 1 1 1 1 1 0 1 1 0 0 1 0 1 1 1 0 1 0 1 1 1 0 1 0 1 1 0 0 1 0 1 0
 1 1 0 1 0 0 1 1 1 0 1 1 0 1 0 1 0 1 0 0 1 1 0 1 1 1 1 1 0 1 1 0 1 1 1 0 1
 1 1 1 0 1 0 1 0 1 0 0 1 1 0 1 1 1 1 0 1 1 1 0 0 1 1 1 1 0 1 0 0 1 1]
In [33]:
a = pd.DataFrame()  # 创建一个空DataFrame 
a['预测值'] = list(y_pred)
a['实际值'] = list(y_test)
a.head()
Out[33]:
预测值 实际值
0 1 1
1 0 0
2 0 1
3 1 1
4 0 0
In [34]:
# 获取预测准确度
from sklearn.metrics import accuracy_score
score = accuracy_score(y_pred, y_test)
score
Out[34]:
0.9814814814814815
In [35]:
# 通过模型自带的score()函数也可以获取预测准确度
mlp.score(X_test, y_test)
Out[35]:
0.9814814814814815
In [36]:
# 对输入的评价进行预测
# comment = input('请输入您对本商品的评价:')
comment = '但我的心每分每刻仍然被她占有'
comment = [' '.join(jieba.cut(comment))]
print(comment)
X_try = vect.transform(comment)
y_pred = mlp.predict(X_try.toarray())
print(y_pred)
['但 我 的 心 每分 每刻 仍然 被 她 占有']
[0]

模型对比¶

为了说明神经网络模型的预测效果,下面建立朴素贝叶斯模型与其进行对比

In [37]:
# 朴素贝叶斯模型对比
from sklearn.naive_bayes import GaussianNB
nb_clf = GaussianNB()
nb_clf.fit(X_train,y_train)

y_pred = nb_clf.predict(X_test)
print(y_pred)

from sklearn.metrics import accuracy_score
score = accuracy_score(y_pred, y_test)
print(score)
[1 1 1 1 1 1 1 1 1 1 0 1 1 0 1 1 0 1 1 1 0 1 0 1 1 1 0 1 0 1 1 0 1 1 1 1 1
 1 1 1 1 0 1 1 1 1 0 1 1 0 1 0 1 1 1 0 1 1 1 0 1 1 1 1 1 0 1 1 0 1 1 1 1 1
 1 1 1 0 1 0 1 0 1 1 0 1 1 0 1 1 1 1 0 1 1 1 1 0 1 1 1 1 0 1 1 1 1 1]
0.8703703703703703

获得的模型准确度评分为0.87, 也就是说模型的预测准确度达到了87%, 说明高斯朴素贝叶斯模型的预测效果略逊于MLP神经网络模型

总体来说,神经网络模型是一种非常不错的机器学习模型,其学习速度快,预测效果好,不过与其他传统的机器学习模型相比,其可解释性不高,因而也常被称为“黑盒模型”。基于简单神经网络模型的深度学习目前在众多领域都有广泛的应用,如人脸识别、量化交易等